/**
 * 20.7.14：增加裁切面材质属性设置方法
 * 210824：升级修改【three.js r120-r131】
 * 20220304:[require] to [import]...jordia
 */
import {Vector3,Quaternion,MeshBasicMaterial,AlwaysStencilFunc,BackSide,IncrementWrapStencilOp,FrontSide,DecrementWrapStencilOp,
    Quaternion,Mesh,NotEqualStencilFunc,ReplaceStencilOp,Group,Object3D,Plane,BufferGeometry,BufferAttribute,Line,LineBasicMaterial,
    Matrix4,Float32BufferAttribute} from 'three';

const ZERO_QUATERNION = new Quaternion(0, 0, 0, 1);
const DEFAULT_UP = new Vector3(0, 1, 0);
const ZERO_VECTOR = new Vector3(0, 0, 0);
let BACK_STENCIL_MATERIAL= new MeshBasicMaterial({
    depthWrite: false,
    colorWrite: false,
    stencilWrite: true,
    stencilFunc: AlwaysStencilFunc,
    side : BackSide,
    stencilFail : IncrementWrapStencilOp,
    stencilZFail : IncrementWrapStencilOp,
    stencilZPass : IncrementWrapStencilOp,
});
let FRONT_STENCIL_MATERIAL=new MeshBasicMaterial({
    depthWrite: false,
    colorWrite: false,
    stencilWrite: true,
    stencilFunc: AlwaysStencilFunc,
    side : FrontSide,
    stencilFail : DecrementWrapStencilOp,
    stencilZFail : DecrementWrapStencilOp,
    stencilZPass : DecrementWrapStencilOp,
});
const NULL_FUNCTION=()=>{};
const LOCK_KEY=[
    'clippingPlanes',
    'stencilWrite',
    'stencilRef',
    'stencilFunc',
    'stencilFail',
    'stencilZFail',
    'stencilZPass'
];

/**
 * 平面裁切对象
 * 裁切目标变换后注意矩阵是否更新
 * 注意：内部renderOrder的累加范围为[target.renderOrder -> target.renderOrder+0.1],
 *      即外部渲染顺序需要在target.renderOrder的基础上留出至少0.1的空间，否则渲染顺序可能会出错
 * 流程：
 * 1.实例化PlaneClip对象
 * 2.设置要裁切的目标网格体（Mesh）
 * 3.设置裁切数据
 * 4.初始化
 * 5.在特定位置裁切
 * 6.调试
 * 自动插值如下：
 *      let clip=new PlanesClip(renderer);
 *      clip.target=mesh;
 *      clip.setPlane('plane1',1,new Vector3(0,1,0),new Vector3(0,1,0),0);
 *      clip.setPlane('plane1',0.6,new Vector3(0,0,0),new Vector3(0,1,0),1);
 *      clip.setPlane('plane1',0.3,new Vector3(0,0.5,0),new Vector3(0,1,1).normalize(),0.5);
 *      clip.setPlane('plane2',0.4,new Vector3(0.3,0,0),new Vector3(1,0,0),0.2);
 *      clip.setPlane('plane2',1,new Vector3(0,0,0),new Vector3(1,0,0),0.8);
 *      clip.begin=true;
 *
 *      let p=0;
 *      setInterval(()=>{
 *           p+=0.01;
 *           p=p>1?0:p;
 *           mm.rotation.z+=0.01;
 *           clip.setPrg(p);
 *           clip.debug=true;
 *       },50);
 *
 * 手动控制如下：
 *      let clip=new PlanesClip(renderer);
 *      clip.target=mesh;
 *      clip.setPlane('plane');      其它参数对手动裁切没有影响
 *      clip.setPlane('plane1');
 *      clip.begin=true;
 *
 *      clip.transform('plane',new Vector3(0,0.5,0),new Quaternion().setFromEuler(new Euler(0,0,1.57,'XYZ')),null);
 *                                   置空的参数代表裁切面的该项属性保持当前值
 *      clip.transform('plane1',null,new Quaternion().setFromEuler(new Euler(1,0,0,'XYZ')),0.5);
 *      clip.debug=true;
 *
 *      setInterval(()=>{
            mesh.rotation.z+=0.01;
            mesh.updateMatrixWorld();
            clip.update();               仅裁切目标变换时需更新裁切
        },50);
 *
 */
export default class PlanesClip extends Group{
    constructor(renderer) {
        super();
        this._renderer = renderer;
        this.init();
    }

    //--------------------------------对外属性及方法------------------------------------------

    /**
     * 是否启用调试（将显示裁切路径，裁切面范围及其法向）
     * @param bol
     */
    set debug(bol) {
        if (!this._target) {
            console.warn('nothing to show unless a target be assigned!');
            return;
        }
        if(this._sInfo.length===0){
            console.warn('no data for clipping, use [setPlane] to add some data!');
            return;
        }
        if (bol) {
            this.debug = false;
            this._debug = new Object3D();
            this.add(this._debug);
            this.drawDebug();
        } else {
            if (this._debug === null) return;
            this.remove(this._debug);
            for (let child of this._debug.children) {
                child.geometry.dispose();
                child.material.dispose();
            }
            this._debug = null;
        }
    }

    /**
     * 获取进程值
     * @returns {number|null}
     */
    get progress(){
        return this._prg;
    }

    /**
     * 初始化/重置裁切数据
     * @param bol
     */
    set begin(bol){
        if (this._target === null) {
            console.warn('there is no target to clip, assign one first!');
            return;
        }
        this._began=bol;
        if(bol){
            if (!this._renderer.localClippingEnabled) {
                console.warn('Property [localClippingEnabled] of renderer is disabled, plz enable this property!');
                return;
            }
            if(this._sInfo.length===0){
                console.warn('no data for clipping, use [setPlane] to add some data!');
                return;
            }

            this._target.add(this);
            this._oriMaterial=this._target.material.clone();
            let planes=Object.values(this._sPlanes);
            let mat=this._target.material.clone();
            this._target.material.clippingPlanes=planes;
            mat.stencilWrite= true;
            mat.stencilRef= 0;
            mat.stencilFunc= NotEqualStencilFunc;
            mat.stencilFail= ReplaceStencilOp;
            mat.stencilZFail= ReplaceStencilOp;
            mat.stencilZPass= ReplaceStencilOp;
            let order=this._target.renderOrder;
            let uOrder=0.1/(Object.keys(this._sPlanes).length+1);
            for(let key in this._sPlanes){
                order+=uOrder;
                let bMat=mat.clone();
                bMat.clippingPlanes=planes.filter(p=>p!==this._sPlanes[''+key+'']);
                this.createBoard(''+key+'',bMat);
                this.createPlaneStencilGroup(''+key+'', this._target.geometry, this._sPlanes[''+key+''], order);
                this._boards[''+key+''].renderOrder = order+uOrder*0.1;
                this.add(this._boards[''+key+'']);
                this.add(this._stencilGroup[''+key+'']);
            }
            mat.dispose();
        }else{
            this._target.remove(this);
            this._target.material.dispose();
            this._target.material=this._oriMaterial.clone();
            this._oriMaterial.dispose();
            this.clearPlanes();
        }
    }

    /**
     * 设置或移除裁切目标（有效的裁切目标必须是网格体,设置为裁切目标后不要修改材质）
     * @param target {null|Mesh} null：移除裁切目标，mesh：设置裁切目标
     */
    set target(target) {
        if (!target) {
            if(this._target)this.begin=false;
            this.debug = false;
            this._target = null;
            return;
        }
        if (!target instanceof Mesh) {
            console.warn('target must be an instance of Mesh!');
            return;
        }
        if (!target.material.hasOwnProperty('clippingPlanes')) {
            console.warn('the material of target has no property [clippingPlanes]!');
            return;
        }
        this._target = target;
        BACK_STENCIL_MATERIAL.transparent=FRONT_STENCIL_MATERIAL.transparent=target.material.transparent;
        BACK_STENCIL_MATERIAL.depthTest=FRONT_STENCIL_MATERIAL.depthTest=target.material.depthTest;
    }

    /**
     * 设置裁切面材质属性（内部为引用赋值，如有必要，自行在外部进行值的克隆）
     * @param key {String} 属性键名
     * @param value {*} 相应属性值
     * @param isOriginalUniform {boolean} 是否为原始uniform值
     * @param needsUpdate {boolean} 是否更新材质
     */
    setMaterial(key,value,isOriginalUniform=false,needsUpdate=false){
        if(LOCK_KEY.indexOf(key)>-1){
            console.warn('property ['+key+'] is not allowed to change!');
            return;
        }
        for(let k in this._boards){
            let mat=this._boards[''+k+''].material;
            if(isOriginalUniform){
                if(!mat.uniforms.hasOwnProperty(key)){
                    console.warn(''+key+' is not a original uniform property of ',mat);
                    return;
                }
                mat.uniforms[''+key+''].value=value;
            }else{
                if(!mat.hasOwnProperty(key)){
                    console.warn(''+key+' is not a property of ',mat);
                    return;
                }
                mat[''+key+'']=value;
            }
            if(needsUpdate)mat.needsUpdate=true;
        }
    }

    /**
     * 手动更改裁切条件
     * @param key {String} 裁切面键名
     * @param position {Vector3|null} 位置
     * @param rotation {Quaternion|null} 旋转
     * @param scale {Number|null} 缩放
     */
    transform(key,position=null,rotation=null,scale=null) {
        if(!this._began){
            console.warn('set [begin=true] to initialize first!');
            return;
        }
        if (!this._boards.hasOwnProperty(key)) {
            console.warn('find no plane named ' + key + '!');
            return;
        }
        let b = this._boards[key];
        if (position) b.position.copy(position);
        if (rotation) b.quaternion.copy(rotation);
        if (scale) b.scale.set(scale,scale,scale);
        if(position||rotation||scale)b.updateMatrixWorld();
        this._tM0.extractRotation(b.matrixWorld);
        this._tV0.copy(b.position).applyMatrix4(this._target.matrixWorld);
        this._tV1.set(0, -1, 0).applyMatrix4(this._tM0);
        this._sPlanes['' + key + ''].setFromNormalAndCoplanarPoint(this._tV1, this._tV0);
    }

    /**
     * 更新裁切面，仅裁切目标变换时需调用以刷新裁切面
     */
    update(){
        for(let key in this._boards){
            let b = this._boards[''+key+''];
            this._tM0.extractRotation(b.matrixWorld);
            this._tV0.copy(b.position).applyMatrix4(this._target.matrixWorld);
            this._tV1.set(0, -1, 0).applyMatrix4(this._tM0);
            this._sPlanes['' + key + ''].setFromNormalAndCoplanarPoint(this._tV1, this._tV0);
        }
    }

    /**
     * 在位置t进行裁切
     * @param t {number} 范围应为0-1
     */
    setPrg(t) {
        if(!this._began){
            console.warn('set [begin=true] to initialize first!');
            return;
        }
        this._prg=t<0?0:(t>1?1:t);
        this.getInfo(this._prg);
        this.updateBoard();
    }

    /**
     * 设置裁切数据
     * @param key {string}       键名
     * @param scale {number|null}     平面尺寸
     * @param position {Vector3|null}  平面位置
     * @param normal {Vector3|null}   平面法线（单位化）
     * @param t {number|null}        进程值（0-1）
     */
    setPlane(key,scale=null,position=null,normal=null,t=null){
        if(!this._sPlanes.hasOwnProperty(key)){
            this._sPlanes[''+key+'']=new Plane().setFromNormalAndCoplanarPoint(normal||DEFAULT_UP,position||ZERO_VECTOR);
        }
        this._sInfo.push([key,scale||1,position||ZERO_VECTOR,normal||DEFAULT_UP,t?t:0]);
    }

    /**
     * 清除裁切数据（平面，封面，模板等）
     */
    clearPlanes(){
        this._began=false;
        this._prg=0;
        this._sInfo.length=0;

        for(let key in this._sPlanes){
            delete this._sPlanes[''+key+''];
        }
        for(let key in this._boards){
            this._boards[''+key+''].geometry.dispose();
            this._boards[''+key+''].material.dispose();
            delete this._boards[''+key+''];
        }
        for(let c of this._stencilGroup.children){
            this._stencilGroup.remove(c);
            for(let sm of c.children) {
                sm.geometry.dispose();
                sm.material.dispose();
            }
        }
    }

    /**
     * 销毁
     */
    dispose() {
        this.clearPlanes();
        this.target=null;
        this.debug = false;

        this._began=null;
        this._debug = null;
        this._prg = null;
        this._tQ0 = null;
        this._tQ1 = null;
        this._tV0 = null;
        this._tV1 = null;
        this._tM0 = null;
        this._stencilGroup=null;
        this._sPlanes=null;
        this._sInfo=null;
        this._boards=null;
        this._lerps=null;
        this._oriMaterial=null;
    }


    //--------------------------------私有属性及方法------------------------------------------

    /**
     * 设置相应键名的平面的裁切模板
     * @param key
     * @param geometry
     * @param plane
     * @param renderOrder
     */
    createPlaneStencilGroup(key, geometry, plane, renderOrder ){
        let gp=new Group();
        // back faces
        let matBack=BACK_STENCIL_MATERIAL.clone();
        matBack.clippingPlanes = [ plane ];
        let meshBack = new Mesh( geometry, matBack );
        meshBack.renderOrder = renderOrder;
        meshBack.raycast=NULL_FUNCTION;
        gp.add( meshBack );

        // front faces
        let matFront=FRONT_STENCIL_MATERIAL.clone();
        matFront.clippingPlanes=[plane];
        let meshFront = new Mesh( geometry, matFront );
        meshFront.raycast=NULL_FUNCTION;
        meshFront.renderOrder = renderOrder;
        gp.add( meshFront );

        this._stencilGroup[key]=gp;
    }

    /**
     * 更新裁切面
     * @private
     */
    updateBoard() {
        for(let i of this._lerps){
            let k=i[0];
            let b=this._boards[''+k+''];
            b.scale.set(i[1],i[1],i[1]);
            b.position.copy(i[2]);
            b.quaternion.setFromUnitVectors(DEFAULT_UP, i[3]);

            b.updateMatrixWorld();
            this._tM0.extractRotation(b.matrixWorld);
            this._tV0.copy(i[2]).applyMatrix4(this._target.matrixWorld);
            this._tV1.set(0, -1, 0).applyMatrix4(this._tM0);
            this._sPlanes[''+k+''].setFromNormalAndCoplanarPoint(this._tV1, this._tV0);
        }
    }

    /**
     * 获取插值数据
     * @param t
     * @private
     */
    getInfo(t) {
        this._lerps.length=0;
        for(let key in this._sPlanes){
            this.lerpKey(''+key+'',t);
        }
    }

    /**
     * 在两个数据之间进行插值
     * @param t
     * @param si
     * @param ei
     * @returns {[]}
     */
    lerpInfo(t, si, ei) {
        let out=[];
        out[0] = (ei[1] - si[1]) * t + si[1];
        out[1]=new Vector3().lerpVectors(si[2], ei[2], t);
        this._tQ0.setFromUnitVectors(si[3], ei[3]);
        Quaternion.slerp(ZERO_QUATERNION, this._tQ0, this._tQ1, t);
        out[2]=new Vector3().copy(si[3]).applyQuaternion(this._tQ1);
        return out;
    }

    /**
     * 对相应键名的数据进行插值
     * @param key
     * @param t
     */
    lerpKey(key,t){
        let s=0,e=1;
        let si=null,ei=null;
        for(let info of this._sInfo){
            if(info[0]!==key)continue;
            if(info[4]<=t&&s<=info[4]){
                s=info[4];
                si=info;
            }
            if(info[4]>=t&&e>=info[4]){
                e=info[4];
                ei=info;
            }
        }
        if(!si){
            si=ei;
        }else if(!ei){
            ei=si;
        }
        let p=e-s<=0?0:(t-s)/(e-s);
        this._lerps.push([key,...this.lerpInfo(p,si,ei)])
    }

    /**
     * 绘制调试
     * @private
     */
    drawDebug() {
        for(let key in this._boards){
            let b=this._boards[''+key+''];
            let g=b.clone();
            g.material=new MeshBasicMaterial({
                side:FrontSide,
                wireframe:true,
            });
            g.raycast=NULL_FUNCTION;

            // 20210824
            // let lineGeo=new Geometry();
            // lineGeo.vertices.push(ZERO_VECTOR,new Vector3(0,0.2,0));
            let lineGeo=new BufferGeometry();
            lineGeo.setAttribute('position',new BufferAttribute(new Float32Array([0,0,0,0,0.2,0]),3));

            let line=new Line(
                lineGeo,
                new LineBasicMaterial());
            line.raycast=NULL_FUNCTION;
            g.add(line);

            this._debug.add(g);
        }
    }

    /**
     * 初始化
     * @private
     */
    init() {
        this.initVariable();
    }

    /**
     * 初始化变量
     * @private
     */
    initVariable() {
        this._target = null;
        this._began=false;
        this._debug = new Object3D();
        this._stencilGroup={};
        this._sPlanes={};
        this._sInfo=[];
        this._boards={};
        this._lerps=[];
        this._prg = 0;
        this._tQ0 = new Quaternion();
        this._tQ1 = new Quaternion();
        this._tV0 = new Vector3();
        this._tV1 = new Vector3();
        this._tM0 = new Matrix4();
        this._oriMaterial=null;
    }

    /**
     * 创建裁切面
     * @private key
     * @private material
     */
    createBoard(key,material) {
        let g = new BufferGeometry();
        let ps = new Float32Array([-1, 0, 1, -1, 0, -1, 1, 0, -1, 1, 0, 1]);
        let uv = new Float32Array([0, 0, 0, 1, 1, 1, 1, 0]);
        g.setIndex([0, 2, 1, 0, 3, 2]);
        g.setAttribute('position', new Float32BufferAttribute(ps, 3));
        g.setAttribute('uv', new Float32BufferAttribute(uv, 2));
        g.computeVertexNormals();
        g.computeBoundingSphere();
        g.setDrawRange(0, Number.POSITIVE_INFINITY);
        let board = new Mesh(g, material);
        board.raycast=NULL_FUNCTION;
        board.onAfterRender=()=>{
            this._renderer.clearStencil();
        };
        this._boards[''+key+'']=board;
    }
}